/*
 * @(#)FSEMExample.java	1.0 Apr 27, 2008
 *
 *	The MIT License
 *
 *	Copyright (c) 2008 Malachi de AElfweald <malachid@gmail.com>
 *
 *	Permission is hereby granted, free of charge, to any person obtaining a copy
 *	of this software and associated documentation files (the "Software"), to deal
 *	in the Software without restriction, including without limitation the rights
 *	to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 *	copies of the Software, and to permit persons to whom the Software is
 *	furnished to do so, subject to the following conditions:
 *
 *	The above copyright notice and this permission notice shall be included in
 *	all copies or substantial portions of the Software.
 *
 *	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 *	IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 *	FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 *	AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 *	LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 *	OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 *	THE SOFTWARE.
 */
package org.eoti.swing;

import org.eoti.util.ShutdownHookListener;
import org.eoti.util.RuntimeUtil;
import org.eoti.swing.util.FPSTimer;
import org.eoti.awt.CursorUtil;

import javax.swing.*;
import java.awt.image.*;
import java.awt.*;
import java.awt.event.*;
import java.util.concurrent.locks.*;
import java.io.PrintStream;

public abstract class FSEMFrame
implements ActiveRenderer, ImageObserver, ShutdownHookListener
{
	protected GraphicsEnvironment gfxEnv;
	protected GraphicsDevice		gfxDevice;
	protected DisplayMode			afsMode, fsemMode;

	protected AFSFrame				afsFrame;
	protected JFrame fsemFrame;

	protected ReentrantLock modeChange;
	protected boolean				isFullScreen				= false;

	protected FPSTimer fpsTimer;
	protected int					reqFPS;

	public FSEMFrame(int requestedFramesPerSecond)
	{
		afsFrame = new AFSFrame();
		fsemFrame = new JFrame();
		initFrames(requestedFramesPerSecond);
	}

	public FSEMFrame(GraphicsConfiguration gc, int requestedFramesPerSecond)
	{
		afsFrame = new AFSFrame(gc);
		fsemFrame = new JFrame(gc);
		initFrames(requestedFramesPerSecond);
	}

	public FSEMFrame(String title, int requestedFramesPerSecond)
	{
		afsFrame = new AFSFrame(title);
		fsemFrame = new JFrame(title);
		initFrames(requestedFramesPerSecond);
	}

	public FSEMFrame(String title, GraphicsConfiguration gc, int requestedFramesPerSecond)
	{
		afsFrame = new AFSFrame(title, gc);
		fsemFrame = new JFrame(title, gc);
		initFrames(requestedFramesPerSecond);
	}

	protected void initFrames(int requestedFramesPerSecond)
	{
		reqFPS = requestedFramesPerSecond;
		modeChange = new ReentrantLock();
		fpsTimer = new FPSTimer(this, reqFPS);
		readyForTermination();
		gfxEnv = GraphicsEnvironment.getLocalGraphicsEnvironment();
		gfxDevice = gfxEnv.getDefaultScreenDevice();

		afsFrame.setLocation(0,0);
		afsFrame.maximize();
		afsFrame.setIgnoreRepaint(true);
		afsFrame.setResizable(false);
		afsFrame.setVisible(true);
		afsMode = gfxDevice.getDisplayMode();

		fsemFrame.setUndecorated(true);
		fsemFrame.setIgnoreRepaint(true);
		fsemFrame.setResizable(false);
		fsemMode = gfxDevice.getDisplayMode();
		setFullScreen(true);

		fpsTimer.start();
	}

	public Insets getInsets()
	{
		if(isFullScreen) return fsemFrame.getInsets();
		return afsFrame.getInsets();
	}

	public Rectangle getBounds()
	{
		if(isFullScreen) return fsemFrame.getBounds();
		return afsFrame.getInsetBounds();
	}

	public BufferStrategy getBufferStrategy()
	{
		if(isFullScreen) return fsemFrame.getBufferStrategy();
		return afsFrame.getBufferStrategy();
	}

	protected void readyForTermination()
	{
        RuntimeUtil.addShutdownHook(this);
	}

    public void shutdown()
    {
        fpsTimer.end();
        setFullScreen(false);
    }

    public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height)
	{
		if(isFullScreen)
			return fsemFrame.imageUpdate(img, infoflags, x, y, width, height);

		return afsFrame.imageUpdate(img, infoflags, x, y, width, height);
	}

	public void setCursor(Cursor cursor)
	{
		afsFrame.setCursor(cursor);
		fsemFrame.setCursor(cursor);
	}

	public void setCursor(CursorUtil.CursorType cursorType)
	{
		setCursor(cursorType.getCursor());
	}

	// ----------- START FULLSCREEN METHODS ---------------

	public boolean isFullScreen(){return isFullScreen;}

	public void toggleFullScreen(){setFullScreen(!isFullScreen);}

	public void setFullScreen(boolean enableFullScreen)
	{
		if(enableFullScreen == isFullScreen)
			return; // no change

		modeChange.lock();

		if(enableFullScreen)
		{
			if(!gfxDevice.isFullScreenSupported())
				return; // unable to change to full screen, so don't change

			afsFrame.setVisible(false);
			gfxDevice.setFullScreenWindow(fsemFrame);
			try{
		    	if(fsemMode != null)
		    		gfxDevice.setDisplayMode(fsemMode);
		    }catch(Exception e){
		    	System.out.format("SET-FULL: Exception: %s", e.getMessage());
//		    	e.printStackTrace();
		    }
		    fsemFrame.setVisible(true);
			fsemFrame.createBufferStrategy(3); // do this only once?
		}else{
			if(gfxDevice.getFullScreenWindow() != null)
			{
				try{
		    		gfxDevice.setDisplayMode(afsMode);
					gfxDevice.setFullScreenWindow(null);
			    }catch(Exception e){
			    	System.out.format("SET-WINDOW: Exception: %s", e.getMessage());
//			    	e.printStackTrace();
			    }
			}
			fsemFrame.setVisible(false);
			afsFrame.setVisible(true);
			afsFrame.createBufferStrategy(3); // do this only once?
		}

		isFullScreen = enableFullScreen;
		modeChange.unlock();
	}

	// ------------ END FULLSCREEN METHODS ----------------

	// ---------- START LISTENER WRAPPERS -----------------

	public void addMouseListener(MouseListener listener)
	{
		afsFrame.addMouseListener(listener);
		fsemFrame.addMouseListener(listener);
	}

	public void removeMouseListener(MouseListener listener)
	{
		afsFrame.removeMouseListener(listener);
		fsemFrame.removeMouseListener(listener);
	}

	public void addMouseMotionListener(MouseMotionListener listener)
	{
		afsFrame.addMouseMotionListener(listener);
		fsemFrame.addMouseMotionListener(listener);
	}

	public void removeMouseMotionListener(MouseMotionListener listener)
	{
		afsFrame.removeMouseMotionListener(listener);
		fsemFrame.removeMouseMotionListener(listener);
	}

	public void addMouseWheelListener(MouseWheelListener listener)
	{
		afsFrame.addMouseWheelListener(listener);
		fsemFrame.addMouseWheelListener(listener);
	}

	public void removeMouseWheelListener(MouseWheelListener listener)
	{
		afsFrame.removeMouseWheelListener(listener);
		fsemFrame.removeMouseWheelListener(listener);
	}

	public void addKeyListener(KeyListener listener)
	{
		afsFrame.addKeyListener(listener);
		fsemFrame.addKeyListener(listener);
	}

	public void removeKeyListener(KeyListener listener)
	{
		afsFrame.removeKeyListener(listener);
		fsemFrame.removeKeyListener(listener);
	}

	// ----------- END LISTENER WRAPPERS ------------------

	// ---------- START ACTIVE RENDERER METHODS -----------

	public FPSTimer getTimer(){return fpsTimer;}

	/**
     * Here, you would implement something like a game characters move() call
     */
	public abstract void activeUpdate();

	public void activeRender()
	{
		if(!modeChange.tryLock())
			return; // in the middle of changing display modes, don't render

		try{
			BufferStrategy bs = getBufferStrategy();
			Graphics gfx = bs.getDrawGraphics();
			if(!bs.contentsLost())
			{
				activeRender(gfx);
				gfx.dispose();
				bs.show();
			}
	    }catch(Exception e){
			System.out.format("RENDER: Exception: %s", e.getMessage());
//			e.printStackTrace();
	    }

	    modeChange.unlock();
	}

	/**
     * Here, you would draw the actual graphics on the provided context
     */
	public abstract void activeRender(Graphics g);

	// ----------- END ACTIVE RENDERER METHODS ------------

	// ------------ START DISPLAY MODE METHODS ------------

	public void showCapabilities()
	{
		showCapabilities(System.out);
	}

	public void showCapabilities(PrintStream out)
	{
		out.println(toString(gfxDevice.getDefaultConfiguration()));
	}

	public String toString(GraphicsConfiguration gc)
	{
		return String.format(
			"[GraphicsConfiguration: %s%s]",
			toString(gc.getImageCapabilities()),
			toString(gc.getBufferCapabilities())
		);
	}

	public String toString(ImageCapabilities ic)
	{
		return String.format(
			"[ImageCapabilities: [Accelerated: %b][TrueVolatile: %b]]",
			ic.isAccelerated(),
			ic.isTrueVolatile()
		);
	}

	public String toString(BufferCapabilities bc)
	{
		return String.format(
			"[BufferCapabilities: [PageFlipping: %b, %s][FullScreen Required: %b][MultiBuffer Available: %b]]",
			bc.isPageFlipping(),
			bc.getFlipContents(),
			bc.isFullScreenRequired(),
			bc.isMultiBufferAvailable()
		);
	}

	public void showAllModes()
	{
		showAllModes(System.out);
	}

	public void showAllModes(PrintStream out)
	{
		for(DisplayMode mode : gfxDevice.getDisplayModes())
			out.println(toString(mode));
	}

	public void showCurrentMode()
	{
		showCurrentMode(System.out);
	}

	public void showCurrentMode(PrintStream out)
	{
		out.format("Current Mode: %s\n", toString(gfxDevice.getDisplayMode()));
	}

	public String toString(DisplayMode mode)
	{
		return String.format(
			"[DisplayMode: [Width: %d][Height: %d][BitDepth: %d][RefreshRate: %d]]",
			mode.getWidth(),
			mode.getHeight(),
			mode.getBitDepth(),
			mode.getRefreshRate()
		);
	}

	public boolean isModeAvailable(int width, int height, int bitDepth)
	{
		for(DisplayMode mode : gfxDevice.getDisplayModes())
		{
			if(mode.getWidth() != width) continue;
			if(mode.getHeight() != height) continue;
			if(mode.getBitDepth() != bitDepth) continue;
			return true;
		}

		return false;
	}

	public void setDisplayMode(int width, int height, int bitDepth)
	throws Exception
	{
		if(!gfxDevice.isDisplayChangeSupported())
			throw new Exception("Display Mode Change not supported!");

		if(!isModeAvailable(width, height, bitDepth))
			throw new Exception(
				String.format(
					"Display Mode (%dx%d @ %d) not supported.",
					width,
					height,
					bitDepth
				)
			);

		fsemMode = new DisplayMode(width, height, bitDepth, DisplayMode.REFRESH_RATE_UNKNOWN);
	}

	// ------------- END DISPLAYMODE METHODS --------------
}